에러 처리 (thiserror, Result, ? 연산자)
NOTE
GlueSQL은 예외(exception) 없이
Result<T, E>값과?연산자만으로 에러를 처리하는 Rust의 표준 방식을 따름.thiserror크레이트로 각 계층(파서/실행/평가 등)마다 별도의 에러 타입을 만들고,#[from]으로 하위 에러를 상위 에러로 자동 변환함.
📌 개념
계층별 에러 enum + thiserror
// core/src/result.rs:16-71 (일부 축약)
#[derive(ThisError, Serialize, Debug, PartialEq)]
pub enum Error {
#[error("storage: {0}")]
StorageMsg(String),
#[error("parser: {0}")]
Parser(String),
#[error("translate: {0}")]
Translate(#[from] TranslateError),
#[error("execute: {0}")]
Execute(#[from] ExecuteError),
#[error("evaluate: {0}")]
Evaluate(#[from] EvaluateError),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;#[derive(ThisError, ...)]:thiserror크레이트의 매크로로,#[error("...")]에 적은 문자열을 바탕으로std::error::Error와Display를 자동 구현해줌 (직접impl Display for Error안 써도 됨)#[error("storage: {0}")]:{0}은 해당 variant의 0번째 필드 값을 메시지에 끼워 넣음 (Parser(String)→"parser: 문법 오류 메시지")#[from] TranslateError: 이 필드에 붙이면TranslateError에 대해impl From<TranslateError> for Error가 자동 생성됨 → 하위 모듈의 에러(TranslateError,ExecuteError,EvaluateError)를?연산자로 던지기만 해도 자동으로 상위Error로 변환됨
모듈별 세부 에러 타입 — EvaluateError
// core/src/executor/evaluate/error.rs:12-161 (일부 축약)
#[derive(ThisError, Serialize, Debug)]
pub enum EvaluateError {
#[error("literal add on non-numeric")]
LiteralAddOnNonNumeric,
#[error("function requires string value: {0}")]
FunctionRequiresStringValue(String),
#[error("identifier not found: {0}")]
IdentifierNotFound(String),
#[error("identifier not found: {table_alias}.{column_name}")]
CompoundIdentifierNotFound { table_alias: String, column_name: String },
}- 위
Error::Evaluate(#[from] EvaluateError)와 짝을 이뤄,EvaluateError가 발생하는 곳에서는Result<T, EvaluateError>를 다루다가, 상위 함수에서?로 반환하는 순간 자동으로Error::Evaluate(...)로 감싸짐 {table_alias}.{column_name}처럼 필드 이름을 직접 메시지에 넣을 수도 있음 (struct-like variant의 경우)
? 연산자로 에러 전파하기
// core/src/glue.rs:26-43 (일부 축약)
pub fn plan_with_params<Sql, I, P>(&mut self, sql: Sql, params: I) -> Result<Vec<StatementPlan>>
where
Sql: AsRef<str>,
I: IntoIterator<Item = P>,
P: IntoParamLiteral,
{
let parsed = parse(sql)?; // 실패 시 즉시 Err를 반환하고 함수 종료
let params: Vec<ParamLiteral> = params
.into_iter()
.map(IntoParamLiteral::into_param_literal)
.collect();
parsed
.into_iter()
.map(|p| {
translate_with_params(&p, ¶ms)
.and_then(|statement| self.storage.plan(statement.into()))
})
.collect()
}parse(sql)?:parse가Result를 반환할 때,Ok(v)면v를 꺼내 계속 진행하고,Err(e)면 그 자리에서 함수 전체가Err(e)를 반환하며 종료됨. 예외(try/catch)를 언어에 넣는 대신,?라는 문법 설탕(syntax sugar)으로 에러를 “값처럼” 다루는 것이 Rust의 특징.and_then(...):Result가Ok일 때만 다음 연산을 이어서 실행 (실패하면 그대로Err전파) —?를 클로저 체인 안에서 쓰기 어려운 경우 대안으로 사용
커스텀 에러 타입 예시 — IndexError
// core/src/store/index.rs:13-38
#[derive(ThisError, Serialize, Debug, PartialEq, Eq)]
pub enum IndexError {
#[error("table not found: {0}")]
TableNotFound(String),
#[error("index name already exists: {0}")]
IndexNameAlreadyExists(String),
#[error("conflict - update failed - index value")]
ConflictOnEmptyIndexValueUpdate,
}NOTE
왜 async가 아니라
?만으로 충분한가 GlueSQL의 core 실행 엔진은 async/await 없이 완전히 동기(sync)로 작성됨. 에러 발생 여부와 무관하게 함수 호출이 즉시(블로킹) 끝나기 때문에, 비동기 런타임(tokio 등) 없이Result+?만으로 에러 전파가 충분히 처리됨. 일부 저장소 구현체(sled 등)가 내부적으로 다른 방식을 쓸 수 있지만,Store/StoreMut트레잇 시그니처 자체는 동기 함수로 고정되어 있음.